/*******************************************************************************
 * Copyright (c) 2012-2016 Codenvy, S.A.
 * All rights reserved. This program and the accompanying materials
 * are made available under the terms of the Eclipse Public License v1.0
 * which accompanies this distribution, and is available at
 * http://www.eclipse.org/legal/epl-v10.html
 *
 * Contributors:
 *   Codenvy, S.A. - initial API and implementation
 *******************************************************************************/
package org.eclipse.che.ide.maven.tools;

import org.eclipse.che.commons.xml.Element;
import org.eclipse.che.commons.xml.ElementMapper;
import org.eclipse.che.commons.xml.NewElement;

import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import static org.eclipse.che.commons.xml.NewElement.createElement;
import static java.util.Collections.emptyList;

/**
 * The {@literal <build>} element contains project build settings.
 * <p/>
 * Supported next data:
 * <ul>
 * <li>sourceDirectory</li>
 * <li>testSourceDirectory</li>
 * <li>scriptSourceDirectory</li>
 * <li>outputDirectory</li>
 * <li>testOutputDirectory</li>
 * <li>resources</li>
 * </ul>
 *
 * @author Eugene Voevodin
 */
public class Build {

    private static final ElementMapper<Resource> RESOURCE_MAPPER = new ResourceMapper();
    private static final ElementMapper<Plugin>   PLUGIN_MAPPER   = new PluginMapper();

    private String         sourceDirectory;
    private String         testSourceDirectory;
    private String         scriptSourceDirectory;
    private String         outputDirectory;
    private String         testOutputDirectory;
    private List<Resource> resources;
    private List<Plugin>   plugins;

    Element buildElement;

    public Build() {
    }

    Build(Element buildElement) {
        this.buildElement = buildElement;
        sourceDirectory = buildElement.getChildText("sourceDirectory");
        testSourceDirectory = buildElement.getChildText("testSourceDirectory");
        scriptSourceDirectory = buildElement.getChildText("scriptSourceDirectory");
        outputDirectory = buildElement.getChildText("outputDirectory");
        testOutputDirectory = buildElement.getChildText("testOutputDirectory");
        if (buildElement.hasSingleChild("resources")) {
            resources = buildElement.getSingleChild("resources").getChildren(RESOURCE_MAPPER);
        }
        if (buildElement.hasSingleChild("plugins")) {
            plugins = buildElement.getSingleChild("plugins").getChildren(PLUGIN_MAPPER);
        }
    }

    /**
     * Returns path to directory where compiled application classes are placed.
     */
    public String getOutputDirectory() {
        return outputDirectory;
    }

    /**
     * Returns path to directory containing the script sources of the project.
     * <p/>
     * This directory is meant to be different
     * from the sourceDirectory, in that its contents
     * will be copied to the output directory
     * in most cases (since scripts are interpreted rather than compiled).
     */
    public String getScriptSourceDirectory() {
        return scriptSourceDirectory;
    }

    /**
     * Returns path to directory containing the source of the project.
     * <p/>
     * The generated build system will compile the source
     * in this directory when the project is built.
     * The path given is relative to the project descriptor.
     */
    public String getSourceDirectory() {
        return sourceDirectory;
    }

    /**
     * Returns path to directory where compiled test classes are placed.
     */
    public String getTestOutputDirectory() {
        return testOutputDirectory;
    }

    /**
     * Returns path to  directory containing the unit test source of the project.
     * <p/>
     * The generated build system will compile
     * these directories when the project is
     * being tested. The path given is relative to the
     * project descriptor.
     */
    public String getTestSourceDirectory() {
        return testSourceDirectory;
    }

    /**
     * Sets the path to directory where compiled application classes are placed.
     */
    public Build setOutputDirectory(String outputDirectory) {
        this.outputDirectory = outputDirectory;
        if (!isNew()) {
            if (outputDirectory == null) {
                buildElement.removeChild("outputDirectory");
            } else if (buildElement.hasSingleChild("outputDirectory")) {
                buildElement.getSingleChild("outputDirectory").setText(outputDirectory);
            } else {
                buildElement.appendChild(createElement("outputDirectory", outputDirectory));
            }
        }
        return this;
    }

    /**
     * Sets the path to directory containing the script sources of the project
     * <p/>
     * If {@code scriptSourceDirectory} is {@code null} and this build instance is associated with
     * xml element then {@code scriptSourceDirectory} will be removed from model as well as from xml.
     *
     * @param scriptSourceDirectory
     *         new build script source directory
     * @return this build instance
     */
    public Build setScriptSourceDirectory(String scriptSourceDirectory) {
        this.scriptSourceDirectory = scriptSourceDirectory;
        if (!isNew()) {
            if (scriptSourceDirectory == null) {
                buildElement.removeChild("scriptSourceDirectory");
            } else if (buildElement.hasSingleChild("scriptSourceDirectory")) {
                buildElement.getSingleChild("scriptSourceDirectory").setText(scriptSourceDirectory);
            } else {
                buildElement.appendChild(createElement("scriptSourceDirectory", scriptSourceDirectory));
            }
        }
        return this;
    }

    /**
     * Sets the path to directory containing the source of the project.
     * <p/>
     * If {@code sourceDirectory} is {@code null} and this build instance is associated with
     * xml element then {@code sourceDirectory} will be removed from model as well as from xml.
     *
     * @param sourceDirectory
     *         new build source directory
     */
    public Build setSourceDirectory(String sourceDirectory) {
        this.sourceDirectory = sourceDirectory;
        if (!isNew()) {
            if (sourceDirectory == null) {
                buildElement.removeChild("sourceDirectory");
            } else if (buildElement.hasSingleChild("sourceDirectory")) {
                buildElement.getSingleChild("sourceDirectory").setText(sourceDirectory);
            } else {
                buildElement.appendChild(createElement("sourceDirectory", sourceDirectory));
            }
        }
        return this;
    }

    /**
     * Sets the path to directory where compiled test classes are placed.
     * <p/>
     * If {@code testOutputDirectory} is {@code null} and this build instance is associated with
     * xml element then {@code testOutputDirectory} will be removed from model as well as from xml.
     *
     * @param testOutputDirectory
     *         new build test output directory
     * @return this build instance
     */
    public Build setTestOutputDirectory(String testOutputDirectory) {
        this.testOutputDirectory = testOutputDirectory;
        if (!isNew()) {
            if (testOutputDirectory == null) {
                buildElement.removeChild("testOutputDirectory");
            } else if (buildElement.hasSingleChild("testOutputDirectory")) {
                buildElement.getSingleChild("testOutputDirectory").setText(testOutputDirectory);
            } else {
                buildElement.appendChild(createElement("testOutputDirectory", testOutputDirectory));
            }
        }
        return this;
    }

    /**
     * Sets the path to directory containing the unit test source of the project.
     * <p/>
     * If {@code testSourceDirectory} is {@code null} and this build instance is associated with
     * xml element then {@code testSourceDirectory} will be removed from model as well as from xml.
     *
     * @param testSourceDirectory
     *         new build test source directory
     * @return this build instance
     */
    public Build setTestSourceDirectory(String testSourceDirectory) {
        this.testSourceDirectory = testSourceDirectory;
        if (!isNew()) {
            if (testSourceDirectory == null) {
                buildElement.removeChild("testSourceDirectory");
            } else if (buildElement.hasSingleChild("testSourceDirectory")) {
                buildElement.getSingleChild("testSourceDirectory").setText(testSourceDirectory);
            } else {
                buildElement.appendChild(createElement("testSourceDirectory", testSourceDirectory));
            }
        }
        return this;
    }

    /**
     * Returns list of resource elements which contains information
     * about where associated with project files should be included
     * <p/>
     * <b>Note: update methods should not be used on returned list</b>
     */
    public List<Resource> getResources() {
        if (resources == null) {
            return emptyList();
        }
        return new ArrayList<>(resources);
    }

    /**
     * Sets build resources, each resource contains information about where
     * associated with project files should be included.
     * <p/>
     * If {@code resources} is {@code null} or <i>empty</i> and this build instance is associated with
     * xml element then {@code resources} will be removed from model as well as from xml.
     *
     * @param resources
     *         new build resources
     * @return this build instance
     */
    public Build setResources(Collection<? extends Resource> resources) {
        if (resources == null || resources.isEmpty()) {
            removeResources();
        } else {
            setResources0(resources);
        }
        return this;
    }

    /**
     * Returns build plugins.
     * <p/>
     * <b>Note: update methods should not be used on returned list</b>
     *
     * @return build plugins or empty map when build doesn't have plugins
     */
    public List<Plugin> getPlugins() {
        if (plugins == null) {
            return emptyList();
        }
        return new ArrayList<>(plugins);
    }

    /**
     * Returns build plugins mapped as {@code plugin.getId() -> plugin}
     *
     * @return mapped plugins or empty map if build doesn't have plugins
     * @see Plugin#getId()
     */
    public Map<String, Plugin> getPluginsAsMap() {
        final Map<String, Plugin> pluginsMap = new HashMap<>();
        for (Plugin plugin : plugins()) {
            pluginsMap.put(plugin.getId(), plugin);
        }
        return pluginsMap;
    }

    /**
     * Sets build plugins.
     *
     * @param plugins
     *         new build plugins, if {@code plugins} parameter is {@code null}
     *         and build associated with xml element then plugins will be removed
     *         from xml as well as from model
     * @return this build instance
     */
    public Build setPlugins(Collection<? extends Plugin> plugins) {
        if (plugins == null || plugins.isEmpty()) {
            removePlugins();
        } else {
            setPlugins0(plugins);
        }
        return this;
    }

    private void removePlugins() {
        if (!isNew()) {
            buildElement.removeChild("plugins");
        }
        plugins = null;
    }

    private void setPlugins0(Collection<? extends Plugin> plugins) {
        this.plugins = new ArrayList<>(plugins);

        if (isNew()) return;
        //if plugins element exists we should replace it children
        //with new set of plugins, otherwise create element for it
        if (buildElement.hasSingleChild("plugins")) {
            //remove "plugins" element children
            final Element pluginsElement = buildElement.getSingleChild("plugins");
            for (Element plugin : pluginsElement.getChildren()) {
                plugin.remove();
            }
            //append each new plugin to "plugins" element
            for (Plugin plugin : plugins) {
                pluginsElement.appendChild(plugin.asXMLElement());
            }
        } else {
            buildElement.appendChild(newPluginsElement(plugins));
        }
    }

    private List<Plugin> plugins() {
        return plugins == null ? plugins = new ArrayList<>() : plugins;
    }

    NewElement asXMLElement() {
        final NewElement xmlBuildElement = createElement("build");
        if (sourceDirectory != null) {
            xmlBuildElement.appendChild(createElement("sourceDirectory", sourceDirectory));
        }
        if (testSourceDirectory != null) {
            xmlBuildElement.appendChild(createElement("testSourceDirectory", testSourceDirectory));
        }
        if (scriptSourceDirectory != null) {
            xmlBuildElement.appendChild(createElement("scriptSourceDirectory", scriptSourceDirectory));
        }
        if (outputDirectory != null) {
            xmlBuildElement.appendChild(createElement("outputDirectory", outputDirectory));
        }
        if (testOutputDirectory != null) {
            xmlBuildElement.appendChild(createElement("testOutputDirectory", testOutputDirectory));
        }
        if (resources != null && !resources.isEmpty()) {
            xmlBuildElement.appendChild(newResourcesElement(resources));
        }
        if (plugins != null && !plugins.isEmpty()) {
            xmlBuildElement.appendChild(newPluginsElement(plugins));
        }
        return xmlBuildElement;
    }

    private boolean isNew() {
        return buildElement == null;
    }

    private NewElement newPluginsElement(Collection<? extends Plugin> plugins) {
        final NewElement xmlPlugins = createElement("plugins");
        for (Plugin plugin : plugins) {
            xmlPlugins.appendChild(plugin.asXMLElement());
        }
        return xmlPlugins;
    }

    private NewElement newResourcesElement(List<Resource> resources) {
        final NewElement resourcesElement = createElement("resources");
        for (Resource resource : resources) {
            resourcesElement.appendChild(resource.asXMLElement());
        }
        return resourcesElement;
    }

    private void setResources0(Collection<? extends Resource> resources) {
        this.resources = new ArrayList<>(resources);

        if (isNew()) return;
        //if resources element exists we should replace it children
        //with new set of resources, otherwise create element for it
        if (buildElement.hasSingleChild("resources")) {
            //remove "resources" element children
            final Element resourcesElement = buildElement.getSingleChild("resources");
            for (Element resource : resourcesElement.getChildren()) {
                resource.remove();
            }
            //append each new resource to "resources" element
            for (Resource resource : resources) {
                resourcesElement.appendChild(resource.asXMLElement());
                resource.resourceElement = resourcesElement.getLastChild();
            }
        } else {
            buildElement.appendChild(newResourcesElement(this.resources));
        }
    }

    private void removeResources() {
        if (!isNew()) {
            buildElement.removeChild("resources");
        }
        this.resources = null;
    }

    private static class ResourceMapper implements ElementMapper<Resource> {

        @Override
        public Resource map(Element element) {
            return new Resource(element);
        }
    }

    private static class PluginMapper implements ElementMapper<Plugin> {

        @Override
        public Plugin map(Element element) {
            return new Plugin(element);
        }
    }
}
